Django REST Framework - Modul 4
Von Session-Auth über Token-Auth bis JWT
Authentifizierung = "Wer bist du?"
Autorisierung = "Was darfst du?"
Beispiel:
Username: john_doe
Password: secret123
→ User ist "John Doe"
Beispiel:
User: john_doe
Role: Editor
→ Darf Filme erstellen & bearbeiten
→ Darf Filme NICHT löschen
1. Authentication: User identifizieren (Login)
↓
2. Authorization: Rechte prüfen (Permissions)
↓
3. Access: Zugriff erlauben oder verweigern
HTTP Request
↓
DRF Authentication Classes prüfen Request
↓
request.user wird gesetzt (User-Objekt oder AnonymousUser)
↓
request.auth wird gesetzt (Token, None, etc.)
↓
View verarbeitet Request
↓
Permission Classes prüfen request.user
↓
Response wird zurückgegeben
from rest_framework.authentication import (
SessionAuthentication, # ← Cookie-basiert (Browser)
BasicAuthentication, # ← Username:Password in Header
TokenAuthentication, # ← Token in Header
)
Gut für:
Cookie-basiert
Gut für:
Token in Header
Gut für:
Empfohlen!
# Neues Projekt erstellen
django-admin startproject movieapi .
python manage.py startapp movies
# Projektstruktur:
movieapi/
├── movieapi/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── movies/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ └── admin.py
└── manage.py
# Installation
pip install djangorestframework
# Installierte Version prüfen
pip show djangorestframework
# filepath: movieapi/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# DRF
'rest_framework',
# Unsere App
'movies',
]
# filepath: movies/models.py
from django.db import models
class Movie(models.Model):
"""Model für Filme"""
title = models.CharField(max_length=200, verbose_name="Titel")
year = models.IntegerField(verbose_name="Erscheinungsjahr")
genre = models.CharField(max_length=100, verbose_name="Genre", blank=True)
rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True, verbose_name="Bewertung")
description = models.TextField(verbose_name="Beschreibung", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Film"
verbose_name_plural = "Filme"
ordering = ['-year', 'title']
def __str__(self):
return f"{self.title} ({self.year})"
class Artist(models.Model):
"""Model für Schauspieler/Künstler"""
first_name = models.CharField(max_length=100, verbose_name="Vorname")
last_name = models.CharField(max_length=100, verbose_name="Nachname")
birth_date = models.DateField(verbose_name="Geburtsdatum", null=True, blank=True)
nationality = models.CharField(max_length=100, verbose_name="Nationalität", blank=True)
biography = models.TextField(verbose_name="Biografie", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Künstler"
verbose_name_plural = "Künstler"
ordering = ['last_name', 'first_name']
def __str__(self):
return f"{self.first_name} {self.last_name}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
class MovieCasting(models.Model):
"""Brückentabelle: Welcher Artist spielt in welchem Movie"""
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings', verbose_name="Film")
artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='movie_roles', verbose_name="Künstler")
role_name = models.CharField(max_length=200, verbose_name="Rollenname")
is_main_role = models.BooleanField(default=False, verbose_name="Hauptrolle")
order = models.IntegerField(default=0, verbose_name="Reihenfolge")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Besetzung"
verbose_name_plural = "Besetzungen"
ordering = ['order', 'artist__last_name']
unique_together = [['movie', 'artist', 'role_name']]
def __str__(self):
role_type = "Hauptrolle" if self.is_main_role else "Nebenrolle"
return f"{self.artist.full_name} als {self.role_name} in {self.movie.title} ({role_type})"
# Migrations erstellen
python manage.py makemigrations
# Output:
Migrations for 'movies':
movies/migrations/0001_initial.py
- Create model Movie
- Create model Artist
- Create model MovieCasting
# Migrations anwenden
python manage.py migrate
# Superuser erstellen
python manage.py createsuperuser
# Username: admin
# Email: admin@example.com
# Password: admin123
# filepath: movies/admin.py
from django.contrib import admin
from .models import Movie, Artist, MovieCasting
@admin.register(Movie)
class MovieAdmin(admin.ModelAdmin):
list_display = ['title', 'year', 'genre', 'rating']
list_filter = ['genre', 'year']
search_fields = ['title', 'description']
ordering = ['-year', 'title']
@admin.register(Artist)
class ArtistAdmin(admin.ModelAdmin):
list_display = ['full_name', 'nationality', 'birth_date']
list_filter = ['nationality']
search_fields = ['first_name', 'last_name']
ordering = ['last_name', 'first_name']
@admin.register(MovieCasting)
class MovieCastingAdmin(admin.ModelAdmin):
list_display = ['artist', 'role_name', 'movie', 'is_main_role', 'order']
list_filter = ['is_main_role', 'movie']
search_fields = ['role_name', 'artist__first_name', 'artist__last_name']
ordering = ['order']
# Server starten
python manage.py runserver
# Admin öffnen: http://localhost:8000/admin/
# Login: admin / admin123
# Testdaten erstellen!
# filepath: movies/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Movie, Artist, MovieCasting
class MovieSerializer(serializers.ModelSerializer):
"""Serializer für Movie CRUD"""
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
class ArtistSerializer(serializers.ModelSerializer):
"""Serializer für Artist CRUD"""
class Meta:
model = Artist
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
class MovieCastingSerializer(serializers.ModelSerializer):
"""Serializer für MovieCasting CRUD"""
artist_name = serializers.CharField(source='artist.full_name', read_only=True)
movie_title = serializers.CharField(source='movie.title', read_only=True)
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['id', 'created_at']
class UserSerializer(serializers.ModelSerializer):
"""Serializer für User-Registrierung"""
password = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'})
password2 = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'}, label='Password confirmation')
class Meta:
model = User
fields = ['id', 'username', 'email', 'password', 'password2', 'first_name', 'last_name']
read_only_fields = ['id']
def validate(self, attrs):
"""Passwörter müssen übereinstimmen"""
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs
def create(self, validated_data):
"""User erstellen mit gehashtem Passwort"""
validated_data.pop('password2') # password2 nicht speichern
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data.get('email', ''),
password=validated_data['password'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', '')
)
return user
class UserDetailSerializer(serializers.ModelSerializer):
"""Serializer für User-Details (ohne Passwort)"""
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'is_staff', 'date_joined']
read_only_fields = ['id', 'is_staff', 'date_joined']
1. User sendet Login (Username + Password)
↓
2. Django prüft Credentials
↓
3. Django erstellt Session in DB
↓
4. Django sendet sessionid Cookie an Browser
↓
5. Browser sendet Cookie bei jedem Request
↓
6. Django lädt Session aus DB → request.user
# filepath: movieapi/settings.py
REST_FRAMEWORK = {
# Authentication Classes
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
# Permission Classes (optional)
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from .models import Movie, Artist, MovieCasting
from .serializers import MovieSerializer, ArtistSerializer, MovieCastingSerializer
class MovieViewSet(viewsets.ModelViewSet):
"""ViewSet für Movie CRUD"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
permission_classes = [IsAuthenticatedOrReadOnly] # Lesen für alle, Schreiben nur Auth
@action(detail=False, methods=['get'])
def my_favorites(self, request):
"""Nur für authenticated Users"""
if not request.user.is_authenticated:
return Response(
{'detail': 'Authentication credentials were not provided.'},
status=status.HTTP_401_UNAUTHORIZED
)
# Beispiel: Filme nach Rating sortiert
movies = Movie.objects.filter(rating__gte=8.0).order_by('-rating')[:10]
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
class ArtistViewSet(viewsets.ModelViewSet):
"""ViewSet für Artist CRUD"""
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
class MovieCastingViewSet(viewsets.ModelViewSet):
"""ViewSet für MovieCasting CRUD"""
queryset = MovieCasting.objects.select_related('movie', 'artist').all()
serializer_class = MovieCastingSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)
urlpatterns = router.urls
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# API
path('api/', include('movies.urls')),
# DRF Auth (Login/Logout für Browsable API)
path('api-auth/', include('rest_framework.urls')),
]
# Server starten
python manage.py runserver
# Browser öffnen:
http://localhost:8000/api/
# Oben rechts: "Log in" Button
# Login mit Superuser: admin / admin123
# Jetzt kannst du:
# - Filme erstellen (POST /api/movies/)
# - Filme bearbeiten (PUT /api/movies/1/)
# - Filme löschen (DELETE /api/movies/1/)
# 1. Ohne Login: http://localhost:8000/api/movies/
# → GET funktioniert (Read)
# → POST-Formular nicht sichtbar (nur Auth Users)
# 2. Login: http://localhost:8000/api-auth/login/
# → Login mit admin / admin123
# 3. Nach Login: http://localhost:8000/api/movies/
# → POST-Formular sichtbar!
# → Film erstellen möglich
# 4. Test: Film erstellen
{
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": 8.7,
"description": "A hacker discovers reality is a simulation."
}
# 5. Custom Action testen:
# http://localhost:8000/api/movies/my_favorites/
# → Funktioniert nur wenn eingeloggt!
# 1. CSRF Token holen
curl -c cookies.txt http://localhost:8000/api/
# 2. Login
curl -b cookies.txt -c cookies.txt \
-X POST http://localhost:8000/api-auth/login/ \
-d "username=admin&password=admin123&csrfmiddlewaretoken=CSRF_TOKEN"
# 3. API Request (mit Session Cookie)
curl -b cookies.txt http://localhost:8000/api/movies/
# ← Session Cookie wird automatisch mitgesendet!
Hinweis: Session-Auth ist hauptsächlich für Browsable API gedacht. Für echte Clients (Mobile, SPA) nutze Token-Auth!
1. User sendet Login (Username + Password)
↓
2. Django prüft Credentials
↓
3. Django erstellt Token & speichert in DB
↓
4. Server sendet Token als JSON
↓
5. Client speichert Token (localStorage, Cookie, etc.)
↓
6. Client sendet Token in Header bei jedem Request:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
↓
7. Django lädt User anhand Token → request.user
# Installation: Bereits in DRF enthalten!
# Nur aktivieren in settings.py
# filepath: movieapi/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# DRF
'rest_framework',
'rest_framework.authtoken', # ← Token-Auth aktivieren!
# Unsere App
'movies',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', # Für Browsable API
'rest_framework.authentication.TokenAuthentication', # ← Token-Auth hinzufügen!
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
# Token-Tabelle erstellen
python manage.py migrate
# Output:
Running migrations:
Applying authtoken.0001_initial... OK
Applying authtoken.0002_auto... OK
Applying authtoken.0003_tokenproxy... OK
# Token-Tabelle in DB:
# - auth_token (key, user_id, created)
# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.permissions import AllowAny, IsAuthenticated
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from .serializers import UserSerializer, UserDetailSerializer
@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
"""
User registrieren & Token zurückgeben
POST /api/auth/register/
Body: {"username": "john", "email": "john@example.com", "password": "secret", "password2": "secret"}
"""
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
# Token erstellen
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user': UserDetailSerializer(user).data
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
"""
User login & Token zurückgeben
POST /api/auth/login/
Body: {"username": "john", "password": "secret"}
"""
username = request.data.get('username')
password = request.data.get('password')
if not username or not password:
return Response(
{'error': 'Please provide both username and password'},
status=status.HTTP_400_BAD_REQUEST
)
# User authentifizieren
user = authenticate(username=username, password=password)
if not user:
return Response(
{'error': 'Invalid credentials'},
status=status.HTTP_401_UNAUTHORIZED
)
# Token erstellen/holen
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user': UserDetailSerializer(user).data
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def logout(request):
"""
User logout - Token löschen
POST /api/auth/logout/
Header: Authorization: Token
"""
try:
# Token des aktuellen Users löschen
request.user.auth_token.delete()
return Response({'message': 'Successfully logged out'}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def user_profile(request):
"""
Aktueller User
GET /api/auth/me/
Header: Authorization: Token
"""
serializer = UserDetailSerializer(request.user)
return Response(serializer.data)
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path
from .views import (
MovieViewSet, ArtistViewSet, MovieCastingViewSet,
register, login, logout, user_profile
)
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)
urlpatterns = [
# Auth Endpoints
path('auth/register/', register, name='auth-register'),
path('auth/login/', login, name='auth-login'),
path('auth/logout/', logout, name='auth-logout'),
path('auth/me/', user_profile, name='auth-me'),
] + router.urls
# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import api_view, action, permission_classes
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from .models import Movie, Artist, MovieCasting
from .serializers import (
MovieSerializer, ArtistSerializer, MovieCastingSerializer,
UserSerializer, UserDetailSerializer
)
# ============= Authentication Views =============
@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
"""User registrieren & Token zurückgeben"""
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user': UserDetailSerializer(user).data
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
"""User login & Token zurückgeben"""
username = request.data.get('username')
password = request.data.get('password')
if not username or not password:
return Response(
{'error': 'Please provide both username and password'},
status=status.HTTP_400_BAD_REQUEST
)
user = authenticate(username=username, password=password)
if not user:
return Response(
{'error': 'Invalid credentials'},
status=status.HTTP_401_UNAUTHORIZED
)
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user': UserDetailSerializer(user).data
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def logout(request):
"""User logout - Token löschen"""
try:
request.user.auth_token.delete()
return Response({'message': 'Successfully logged out'}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def user_profile(request):
"""Aktueller User"""
serializer = UserDetailSerializer(request.user)
return Response(serializer.data)
# ============= Resource ViewSets =============
class MovieViewSet(viewsets.ModelViewSet):
"""ViewSet für Movie CRUD"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
@action(detail=False, methods=['get'])
def my_favorites(self, request):
"""Nur für authenticated Users"""
if not request.user.is_authenticated:
return Response(
{'detail': 'Authentication credentials were not provided.'},
status=status.HTTP_401_UNAUTHORIZED
)
movies = Movie.objects.filter(rating__gte=8.0).order_by('-rating')[:10]
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
class ArtistViewSet(viewsets.ModelViewSet):
"""ViewSet für Artist CRUD"""
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
class MovieCastingViewSet(viewsets.ModelViewSet):
"""ViewSet für MovieCasting CRUD"""
queryset = MovieCasting.objects.select_related('movie', 'artist').all()
serializer_class = MovieCastingSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
# cURL:
curl -X POST http://localhost:8000/api/auth/register/ \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"email": "john@example.com",
"password": "secret123",
"password2": "secret123",
"first_name": "John",
"last_name": "Doe"
}'
# Response:
{
"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
"user": {
"id": 2,
"username": "john_doe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"is_staff": false,
"date_joined": "2024-01-15T10:30:00Z"
}
}
# cURL:
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"password": "secret123"
}'
# Response:
{
"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
"user": {
"id": 2,
"username": "john_doe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"is_staff": false,
"date_joined": "2024-01-15T10:30:00Z"
}
}
# Token speichern für weitere Requests!
# cURL:
curl -X GET http://localhost:8000/api/auth/me/ \
-H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
# Response:
{
"id": 2,
"username": "john_doe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"is_staff": false,
"date_joined": "2024-01-15T10:30:00Z"
}
# cURL:
curl -X POST http://localhost:8000/api/movies/ \
-H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" \
-H "Content-Type: application/json" \
-d '{
"title": "Inception",
"year": 2010,
"genre": "Sci-Fi",
"rating": 8.8,
"description": "A thief who steals corporate secrets through dream-sharing technology."
}'
# Response:
{
"id": 1,
"title": "Inception",
"year": 2010,
"genre": "Sci-Fi",
"rating": 8.8,
"description": "A thief who steals corporate secrets through dream-sharing technology.",
"created_at": "2024-01-15T11:00:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
# cURL:
curl -X GET http://localhost:8000/api/movies/
# Funktioniert! → IsAuthenticatedOrReadOnly
# Lesen ist für alle erlaubt
# Response:
[
{
"id": 1,
"title": "Inception",
"year": 2010,
"genre": "Sci-Fi",
"rating": 8.8,
"description": "A thief who steals corporate secrets...",
"created_at": "2024-01-15T11:00:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
]
# cURL:
curl -X POST http://localhost:8000/api/movies/ \
-H "Content-Type: application/json" \
-d '{
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi"
}'
# Response: 401 Unauthorized
{
"detail": "Authentication credentials were not provided."
}
# ← Schreiben nur für authenticated Users!
# cURL:
curl -X POST http://localhost:8000/api/auth/logout/ \
-H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
# Response:
{
"message": "Successfully logged out"
}
# Token wurde aus DB gelöscht!
# Weitere Requests mit diesem Token schlagen fehl.
Stateless, selbst-enthaltene Authentifizierung
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature
# Header (Base64):
{
"alg": "HS256",
"typ": "JWT"
}
# Payload (Base64):
{
"user_id": 1,
"username": "john",
"exp": 1705329000, # Expiration
"iat": 1705325400 # Issued At
}
# Signature (HMAC):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
SECRET_KEY
)
1. Login → Access Token (5-15 min) + Refresh Token (1-7 Tage)
2. API Request mit Access Token
3. Access Token abgelaufen → Refresh Token verwenden
4. Neuer Access Token erhalten
5. Weiter mit neuem Access Token
# Installation
pip install djangorestframework-simplejwt
# Installierte Version prüfen
pip show djangorestframework-simplejwt
# filepath: movieapi/settings.py
from datetime import timedelta
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# DRF
'rest_framework',
'rest_framework.authtoken', # Token-Auth (optional, für backward compatibility)
# Unsere App
'movies',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', # Browsable API
'rest_framework.authentication.TokenAuthentication', # Token-Auth
'rest_framework_simplejwt.authentication.JWTAuthentication', # ← JWT!
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
# JWT Settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15), # Access Token gültig für 15 Minuten
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token gültig für 7 Tage
'ROTATE_REFRESH_TOKENS': True, # Neuer Refresh Token bei Refresh
'BLACKLIST_AFTER_ROTATION': True, # Alte Refresh Tokens blacklisten
'UPDATE_LAST_LOGIN': True, # last_login aktualisieren
'ALGORITHM': 'HS256', # Verschlüsselungs-Algorithmus
'SIGNING_KEY': SECRET_KEY, # Secret Key aus settings
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',), # Authorization: Bearer
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
}
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
from .views import (
MovieViewSet, ArtistViewSet, MovieCastingViewSet,
register, login, logout, user_profile
)
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)
urlpatterns = [
# Token-Auth Endpoints
path('auth/register/', register, name='auth-register'),
path('auth/login/', login, name='auth-login'),
path('auth/logout/', logout, name='auth-logout'),
path('auth/me/', user_profile, name='auth-me'),
# JWT Endpoints
path('auth/jwt/create/', TokenObtainPairView.as_view(), name='jwt-create'),
path('auth/jwt/refresh/', TokenRefreshView.as_view(), name='jwt-refresh'),
path('auth/jwt/verify/', TokenVerifyView.as_view(), name='jwt-verify'),
] + router.urls
POST /api/auth/jwt/create/ → Login (Access + Refresh Token)POST /api/auth/jwt/refresh/ → Refresh Token verwenden → Neuer Access TokenPOST /api/auth/jwt/verify/ → Token validieren# cURL:
curl -X POST http://localhost:8000/api/auth/jwt/create/ \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123"
}'
# Response:
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTkzMDgwMCwidXNlcl9pZCI6MX0.xyz...",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA1MzI2NzAwLCJ1c2VyX2lkIjoxfQ.abc..."
}
# Speichern:
# - access → Für API Requests (15 Minuten gültig)
# - refresh → Für neuen Access Token (7 Tage gültig)
# cURL:
curl -X POST http://localhost:8000/api/movies/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"title": "Interstellar",
"year": 2014,
"genre": "Sci-Fi",
"rating": 8.6,
"description": "A team of explorers travel through a wormhole in space."
}'
# Response:
{
"id": 2,
"title": "Interstellar",
"year": 2014,
"genre": "Sci-Fi",
"rating": 8.6,
"description": "A team of explorers travel through a wormhole in space.",
"created_at": "2024-01-15T12:00:00Z",
"updated_at": "2024-01-15T12:00:00Z"
}
# ✅ Funktioniert mit JWT Access Token!
# Nach 15 Minuten ist Access Token abgelaufen:
curl -X GET http://localhost:8000/api/movies/1/ \
-H "Authorization: Bearer "
# Response: 401 Unauthorized
{
"detail": "Given token not valid for any token type",
"code": "token_not_valid",
"messages": [
{
"token_class": "AccessToken",
"token_type": "access",
"message": "Token is expired"
}
]
}
# Lösung: Refresh Token verwenden
curl -X POST http://localhost:8000/api/auth/jwt/refresh/ \
-H "Content-Type: application/json" \
-d '{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTkzMDgwMCwidXNlcl9pZCI6MX0.xyz..."
}'
# Response: Neuer Access Token!
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA1MzI3NjAwLCJ1c2VyX2lkIjoxfQ.new..."
}
# Jetzt mit neuem Access Token weiterarbeiten!
# Token prüfen ob gültig:
curl -X POST http://localhost:8000/api/auth/jwt/verify/ \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
# Response: 200 OK (Token ist gültig)
{}
# Oder: 401 Unauthorized (Token ungültig/abgelaufen)
{
"detail": "Token is invalid or expired",
"code": "token_not_valid"
}
1. Login → Access Token (15 min) + Refresh Token (7 Tage)
2. API Requests mit Access Token
3. Access Token abgelaufen? → Refresh verwenden
4. Neuer Access Token → Weiter arbeiten
5. Refresh Token abgelaufen? → Neu einloggen
from rest_framework.permissions import (
AllowAny, # Alle dürfen alles
IsAuthenticated, # Nur authenticated Users
IsAuthenticatedOrReadOnly, # Lesen für alle, Schreiben nur Auth
IsAdminUser, # Nur Admin (is_staff=True)
DjangoModelPermissions, # Django Model Permissions
DjangoObjectPermissions, # Object-Level Permissions
)
class MovieViewSet(viewsets.ModelViewSet):
permission_classes = [AllowAny]
# Alle dürfen:
# - GET, POST, PUT, PATCH, DELETE
# Auch ohne Login!
class MovieViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
# Nur authenticated Users dürfen:
# - GET, POST, PUT, PATCH, DELETE
# Ohne Login: 401 Unauthorized
class MovieViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
# Alle dürfen:
# - GET (Read)
# Nur authenticated Users:
# - POST, PUT, PATCH, DELETE (Write)
class MovieViewSet(viewsets.ModelViewSet):
permission_classes = [IsAdminUser]
# Nur Admin (is_staff=True):
# - GET, POST, PUT, PATCH, DELETE
# Normale User: 403 Forbidden
# filepath: movies/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom Permission: Nur Owner darf bearbeiten/löschen
Alle anderen dürfen nur lesen
"""
def has_object_permission(self, request, view, obj):
# Read permissions (GET, HEAD, OPTIONS) für alle
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions nur für Owner
# Annahme: Model hat 'owner' ForeignKey zu User
return obj.owner == request.user
# filepath: movies/models.py
from django.db import models
from django.contrib.auth.models import User
class Movie(models.Model):
title = models.CharField(max_length=200)
year = models.IntegerField()
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='movies') # ← Owner
# ... weitere Felder
# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie
from .serializers import MovieSerializer
from .permissions import IsOwnerOrReadOnly
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
permission_classes = [IsOwnerOrReadOnly]
def perform_create(self, serializer):
# Owner automatisch setzen bei Erstellung
serializer.save(owner=self.request.user)
# filepath: movies/permissions.py
from rest_framework import permissions
class IsAdminOrReadOnly(permissions.BasePermission):
"""
Admin darf alles, andere nur lesen
"""
def has_permission(self, request, view):
# Read permissions für alle
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions nur für Admin
return request.user and request.user.is_staff
# filepath: movies/permissions.py
from rest_framework import permissions
class IsPremiumUser(permissions.BasePermission):
"""
Nur Premium-User (Custom User Model mit is_premium)
"""
message = "You must be a premium user to access this resource."
def has_permission(self, request, view):
# User muss authenticated UND premium sein
return (
request.user and
request.user.is_authenticated and
hasattr(request.user, 'is_premium') and
request.user.is_premium
)
# filepath: movies/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .permissions import IsOwnerOrReadOnly, IsAdminOrReadOnly
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def get_permissions(self):
"""
Unterschiedliche Permissions für unterschiedliche Actions
"""
if self.action == 'list' or self.action == 'retrieve':
# Lesen: Alle
permission_classes = [AllowAny]
elif self.action == 'create':
# Erstellen: Nur authenticated
permission_classes = [IsAuthenticated]
else:
# Update/Delete: Nur Owner oder Admin
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly | IsAdminUser]
return [permission() for permission in permission_classes]
# settings.py (Production)
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Tokens IMMER über HTTPS senden!
# settings.py
import os
from decouple import config # pip install python-decouple
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
# .env Datei:
SECRET_KEY=your-super-secret-key-here
DEBUG=False
# .env NICHT in Git committen!
# .gitignore:
.env
# JWT: Bereits eingebaut
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}
# DRF Token: Keine Expiration!
# → JWT verwenden für Production!
# Installation
pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
'corsheaders',
# ...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
# ...
]
# Production: Nur erlaubte Origins
CORS_ALLOWED_ORIGINS = [
"https://example.com",
"https://www.example.com",
]
# Development: Alle erlauben (NUR für Dev!)
CORS_ALLOW_ALL_ORIGINS = True # ← Nur Dev!
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day', # Anonymous: 100 Requests/Tag
'user': '1000/day' # Authenticated: 1000 Requests/Tag
}
}
# Serializer: Passwörter nie zurückgeben!
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True) # ← write_only!
class Meta:
model = User
fields = ['id', 'username', 'email', 'password']
read_only_fields = ['id']
# Response enthält KEIN password!
# filepath: movies/tests/test_auth.py
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from rest_framework.authtoken.models import Token
class AuthenticationTestCase(TestCase):
"""Tests für Authentication"""
def setUp(self):
"""Test-User erstellen"""
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.token = Token.objects.create(user=self.user)
def test_register_user(self):
"""Test: User registrieren"""
data = {
'username': 'newuser',
'email': 'new@example.com',
'password': 'newpass123',
'password2': 'newpass123'
}
response = self.client.post('/api/auth/register/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn('token', response.data)
self.assertIn('user', response.data)
self.assertEqual(response.data['user']['username'], 'newuser')
def test_login_user(self):
"""Test: User login"""
data = {
'username': 'testuser',
'password': 'testpass123'
}
response = self.client.post('/api/auth/login/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('token', response.data)
self.assertIn('user', response.data)
def test_login_invalid_credentials(self):
"""Test: Login mit falschen Credentials"""
data = {
'username': 'testuser',
'password': 'wrongpass'
}
response = self.client.post('/api/auth/login/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_authenticated_request(self):
"""Test: Request mit Token"""
# Token im Header setzen
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
response = self.client.get('/api/auth/me/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['username'], 'testuser')
def test_unauthenticated_request(self):
"""Test: Request ohne Token"""
response = self.client.post('/api/movies/', {'title': 'Test'}, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_logout(self):
"""Test: Logout (Token löschen)"""
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
response = self.client.post('/api/auth/logout/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Token sollte gelöscht sein
self.assertFalse(Token.objects.filter(user=self.user).exists())
# Tests ausführen:
python manage.py test movies.tests.test_auth
# filepath: movieapi/settings.py
import os
from datetime import timedelta
from decouple import config
# Security
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')
# HTTPS
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# DRF
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
},
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# JWT
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
}
# CORS
CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='').split(',')
# Database (PostgreSQL empfohlen)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST', default='localhost'),
'PORT': config('DB_PORT', default='5432'),
}
}
# .env
SECRET_KEY=your-super-secret-production-key-here
DEBUG=False
ALLOWED_HOSTS=example.com,www.example.com
CORS_ALLOWED_ORIGINS=https://example.com,https://www.example.com
DB_NAME=movieapi_db
DB_USER=movieapi_user
DB_PASSWORD=secure_password
DB_HOST=localhost
DB_PORT=5432
Verwendung:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
}
✅ Vorteile:
❌ Nachteile:
🎯 Gut für:
Verwendung:
INSTALLED_APPS = [
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
✅ Vorteile:
❌ Nachteile:
🎯 Gut für:
Verwendung:
pip install djangorestframework-simplejwt
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}
✅ Vorteile:
❌ Nachteile:
🎯 Gut für:
⭐ Empfohlen für Production!
Implementiere sichere Authentication in deiner API!
Keep coding, keep learning! 💻